/*
 * To change this license header, choose License Headers in Project Properties. To change this template file, choose
 * Tools | Templates and open the template in the editor.
 */
package org.jflame.fx.control;

import java.util.ArrayList;
import java.util.List;

import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.BoundingBox;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;
import javafx.util.Duration;

/**
 * 自定义窗体边框
 * 
 * @author yucan.zhang
 */
public class JFLDecorator extends VBox {

    private Stage primaryStage;

    private double xOffset = 0;
    private double yOffset = 0;
    private double newX, newY;
    private double initX, initY;
    private double initWidth = -1, initHeight = -1;
    private double initStageX = -1, initStageY = -1;

    private boolean allowMove = false;
    private boolean isDragging = false;
    private Timeline windowDecoratorAnimation;
    private StackPane contentPlaceHolder = new StackPane();
    private HBox buttonsContainer;

    private ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>(
            () -> primaryStage.fireEvent(new WindowEvent(primaryStage, WindowEvent.WINDOW_CLOSE_REQUEST)));

    private BooleanProperty customMaximize = new SimpleBooleanProperty(false);
    private boolean maximized = false;
    private BoundingBox originalBox;
    private BoundingBox maximizedBox;

    protected Button btnMax;
    protected Button btnFull;
    protected Button btnClose;
    protected Button btnMin;

    protected StringProperty title = new SimpleStringProperty();
    protected Text text;
    protected Node graphic;
    protected HBox graphicContainer;

    /**
     * Create a window decorator for the specified node with the options: - full screen - maximize - minimize
     *
     * @param stage the primary stage used by the application
     * @param node the node to be decorated
     */
    public JFLDecorator(Stage stage, Node node) {
        this(stage, node, true, true, true);
    }

    /**
     * Create a window decorator for the specified node with the options: - full screen - maximize - minimize
     *
     * @param stage the primary stage used by the application
     * @param node the node to be decorated
     * @param fullScreen indicates whether to show full screen option or not
     * @param max indicates whether to show maximize option or not
     * @param min indicates whether to show minimize option or not
     */
    public JFLDecorator(Stage stage, Node node, boolean fullScreen, boolean max, boolean min) {
        primaryStage = stage;
        // Note that setting the style to TRANSPARENT is causing performance
        // degradation, as an alternative we set it to UNDECORATED instead.transparent
        primaryStage.initStyle(StageStyle.UNDECORATED);

        setPickOnBounds(false);
        getStyleClass().add("jfx-decorator");

        initializeButtons();
        initializeContainers(node, fullScreen, max, min);

        primaryStage.fullScreenProperty()
                .addListener((o, oldVal, newVal) ->
                {
                    if (newVal) {
                        // remove border
                        contentPlaceHolder.getStyleClass()
                                .remove("resize-border");
                        /*
                         * note the border property MUST NOT be bound to another property when going full screen mode, thus the
                         * binding will be lost if exisited
                         */
                        contentPlaceHolder.borderProperty()
                                .unbind();
                        contentPlaceHolder.setBorder(Border.EMPTY);
                        if (windowDecoratorAnimation != null) {
                            windowDecoratorAnimation.stop();
                        }
                        windowDecoratorAnimation = new Timeline(new KeyFrame(Duration.millis(320), new KeyValue(
                                this.translateYProperty(), -buttonsContainer.getHeight(), Interpolator.EASE_BOTH)));
                        windowDecoratorAnimation.setOnFinished((finish) -> {
                            this.getChildren()
                                    .remove(buttonsContainer);
                            this.setTranslateY(0);
                        });
                        windowDecoratorAnimation.play();
                    } else {
                        // add border
                        if (windowDecoratorAnimation != null) {
                            if (windowDecoratorAnimation.getStatus() == Animation.Status.RUNNING) {
                                windowDecoratorAnimation.stop();
                            } else {
                                this.getChildren()
                                        .add(0, buttonsContainer);
                            }
                        }
                        this.setTranslateY(-buttonsContainer.getHeight());
                        windowDecoratorAnimation = new Timeline(new KeyFrame(Duration.millis(320),
                                new KeyValue(this.translateYProperty(), 0, Interpolator.EASE_BOTH)));
                        windowDecoratorAnimation.setOnFinished((finish) -> {
                            contentPlaceHolder.setBorder(new Border(new BorderStroke(Color.BLACK,
                                    BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(0, 4, 4, 4))));
                            contentPlaceHolder.getStyleClass()
                                    .add("resize-border");
                        });
                        windowDecoratorAnimation.play();
                    }
                });

        contentPlaceHolder.addEventHandler(MouseEvent.MOUSE_PRESSED, (mouseEvent) -> updateInitMouseValues(mouseEvent));
        buttonsContainer.addEventHandler(MouseEvent.MOUSE_PRESSED, (mouseEvent) -> updateInitMouseValues(mouseEvent));

        // show the drag cursor on the borders
        addEventFilter(MouseEvent.MOUSE_MOVED, (mouseEvent) -> showDragCursorOnBorders(mouseEvent));

        // handle drag events on the decorator pane
        addEventFilter(MouseEvent.MOUSE_RELEASED, (mouseEvent) -> isDragging = false);
        this.setOnMouseDragged((mouseEvent) -> handleDragEventOnDecoratorPane(mouseEvent));
    }

    private void initializeButtons() {

        SVGGlyph full = new SVGGlyph(0, "FULLSCREEN",
                "M598 214h212v212h-84v-128h-128v-84zM726 726v-128h84v212h-212v-84h128zM214 426v-212h212v84h-128v128h-84zM298 598v128h128v84h-212v-212h84z",
                Color.WHITE);
        full.setSize(16, 16);

        // M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857
        // 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z
        SVGGlyph minus = new SVGGlyph(0, "MINUS",
                "M804.571,407.821l0,84.21401q0,17.54452 -16,29.82576t-38.857,12.28124l-694.857,0q-22.857,0 -38.857,-12.28124t-16,-29.82576l0,-84.21401q0,-17.54452 16,-29.82576t38.857,-12.28124l694.857,0q22.857,0 38.857,12.28124t16,29.82576z",
                Color.WHITE);
        minus.setSize(10, 1);
        minus.setTranslateY(1);
        // M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34
        // 25-60t59-26z
        SVGGlyph resizeMax = new SVGGlyph(0, "RESIZE_MAX",
                "M128.576377 895.420553 128.576377 128.578424l766.846222 0 0 766.842129L128.576377 895.420553zM799.567461 224.434585 224.432539 224.434585l0 575.134923 575.134923 0L799.567461 224.434585z",
                Color.WHITE);
        resizeMax.setSize(10, 10);
        // M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264
        // 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z
        SVGGlyph resizeMin = new SVGGlyph(0, "RESIZE_MIN",
                "M1086.033752 753.710082 878.220684 753.710082 878.220684 951.774989 878.220684 1021.784509 878.220684 1023.113804 808.211164 1023.113804 808.211164 1021.784509 70.895716 1021.784509 70.895716 1023.113804 0.886196 1023.113804 0.886196 1021.784509 0.886196 951.774989 0.886196 339.413241 0.886196 269.403721 70.895716 269.403721 269.403721 269.403721 269.403721 0.886196 274.277802 0.886196 339.413241 0.886196 1086.033752 0.886196 1151.612289 0.886196 1156.043271 0.886196 1156.043271 683.700563 1156.043271 753.710082 1086.033752 753.710082ZM70.895716 951.774989 808.211164 951.774989 808.211164 753.710082 808.211164 683.700563 808.211164 339.413241 70.895716 339.413241 70.895716 951.774989ZM1086.033752 70.895716 339.413241 70.895716 339.413241 269.403721 808.211164 269.403721 878.220684 269.403721 878.220684 339.413241 878.220684 683.700563 1086.033752 683.700563 1086.033752 70.895716Z",
                Color.WHITE);
        resizeMin.setSize(12, 12);
        SVGGlyph close = new SVGGlyph(0, "CLOSE",
                "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z",
                Color.WHITE);
        close.setSize(10, 10);
        btnFull = new Button();
        btnFull.getStyleClass()
                .add("jfx-decorator-button");
        btnFull.setCursor(Cursor.HAND);
        btnFull.setOnAction((action) -> primaryStage.setFullScreen(!primaryStage.isFullScreen()));
        btnFull.setGraphic(full);
        btnFull.setTranslateX(-30);

        btnClose = new Button();
        btnClose.getStyleClass()
                .add("jfx-decorator-button");
        btnClose.setCursor(Cursor.HAND);
        btnClose.setOnAction((action) -> onCloseButtonAction.get()
                .run());
        btnClose.setGraphic(close);
        // btnClose.setRipplerFill(Color.WHITE);

        btnMin = new Button();
        btnMin.getStyleClass()
                .add("jfx-decorator-button");
        btnMin.setCursor(Cursor.HAND);
        btnMin.setOnAction((action) -> primaryStage.setIconified(true));
        btnMin.setGraphic(minus);
        // btnMin.setRipplerFill(Color.WHITE);

        btnMax = new Button();
        btnMax.getStyleClass()
                .add("jfx-decorator-button");
        btnMax.setCursor(Cursor.HAND);
        // btnMax.setRipplerFill(Color.WHITE);
        btnMax.setOnAction((action) -> maximize(resizeMin, resizeMax));
        btnMax.setGraphic(resizeMax);
    }

    private void maximize(SVGGlyph resizeMin, SVGGlyph resizeMax) {
        if (!isCustomMaximize()) {
            primaryStage.setMaximized(!primaryStage.isMaximized());
            maximized = primaryStage.isMaximized();
            if (primaryStage.isMaximized()) {
                btnMax.setGraphic(resizeMin);
                btnMax.setTooltip(new Tooltip("还原"));
            } else {
                btnMax.setGraphic(resizeMax);
                btnMax.setTooltip(new Tooltip("最大化"));
            }
        } else {
            if (!maximized) {
                // store original bounds
                originalBox = new BoundingBox(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(),
                        primaryStage.getHeight());
                // get the max stage bounds
                Screen screen = Screen
                        .getScreensForRectangle(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(),
                                primaryStage.getHeight())
                        .get(0);
                Rectangle2D bounds = screen.getVisualBounds();
                maximizedBox = new BoundingBox(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(),
                        bounds.getHeight());
                // maximized the stage
                primaryStage.setX(maximizedBox.getMinX());
                primaryStage.setY(maximizedBox.getMinY());
                primaryStage.setWidth(maximizedBox.getWidth());
                primaryStage.setHeight(maximizedBox.getHeight());
                btnMax.setGraphic(resizeMin);
                btnMax.setTooltip(new Tooltip("还原"));
                setPadding(Insets.EMPTY);
            } else {
                // restore stage to its original size
                primaryStage.setX(originalBox.getMinX());
                primaryStage.setY(originalBox.getMinY());
                primaryStage.setWidth(originalBox.getWidth());
                primaryStage.setHeight(originalBox.getHeight());
                originalBox = null;
                btnMax.setGraphic(resizeMax);
                btnMax.setTooltip(new Tooltip("最大化"));
            }
            maximized = !maximized;
        }
    }

    private void initializeContainers(Node node, boolean fullScreen, boolean max, boolean min) {
        buttonsContainer = new HBox();
        buttonsContainer.getStyleClass()
                .add("jfx-decorator-buttons-container");
        buttonsContainer
                .setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
        // BINDING
        buttonsContainer.setPadding(new Insets(4));
        buttonsContainer.setAlignment(Pos.CENTER_RIGHT);

        // customize decorator buttons
        // List<JFXButton> btns = new ArrayList<>();
        List<Button> btns = new ArrayList<>();
        if (fullScreen) {
            btns.add(btnFull);
        }
        if (min) {
            btns.add(btnMin);
        }
        if (max) {
            btns.add(btnMax);
            // maximize/restore the window on header double click
            buttonsContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, (mouseEvent) -> {
                if (mouseEvent.getClickCount() == 2) {
                    btnMax.fire();
                }
            });
        }
        btns.add(btnClose);

        text = new Text();
        text.getStyleClass()
                .addAll("jfx-decorator-text", "title", "jfx-decorator-title");
        // text.setFill(Color.WHITE);
        text.textProperty()
                .bind(title); // binds the Text's text to title
        title.bind(primaryStage.titleProperty()); // binds title to the primaryStage's title

        graphicContainer = new HBox();
        graphicContainer.setPickOnBounds(false);
        graphicContainer.setAlignment(Pos.CENTER_LEFT);
        graphicContainer.getChildren()
                .setAll(text);

        HBox graphicTextContainer = new HBox(graphicContainer, text);
        graphicTextContainer.getStyleClass()
                .add("jfx-decorator-title-container");
        graphicTextContainer.setAlignment(Pos.CENTER_LEFT);
        graphicTextContainer.setPickOnBounds(false);
        HBox.setHgrow(graphicTextContainer, Priority.ALWAYS);
        HBox.setMargin(graphicContainer, new Insets(0, 8, 0, 8));

        buttonsContainer.getChildren()
                .setAll(graphicTextContainer);
        buttonsContainer.getChildren()
                .addAll(btns);
        buttonsContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, (enter) -> allowMove = true);
        buttonsContainer.addEventHandler(MouseEvent.MOUSE_EXITED, (enter) -> {
            if (!isDragging) {
                allowMove = false;
            }
        });
        buttonsContainer.setMinWidth(180);
        contentPlaceHolder.getStyleClass()
                .add("jfx-decorator-content-container");
        contentPlaceHolder.setMinSize(0, 0);
        contentPlaceHolder.getChildren()
                .add(node);
        ((Region) node).setMinSize(0, 0);
        VBox.setVgrow(contentPlaceHolder, Priority.ALWAYS);
        contentPlaceHolder.getStyleClass()
                .add("resize-border");
        contentPlaceHolder.setBorder(new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID,
                CornerRadii.EMPTY, new BorderWidths(0, 4, 4, 4))));
        // BINDING

        Rectangle clip = new Rectangle();
        clip.widthProperty()
                .bind(((Region) node).widthProperty());
        clip.heightProperty()
                .bind(((Region) node).heightProperty());
        node.setClip(clip);
        this.getChildren()
                .addAll(buttonsContainer, contentPlaceHolder);
    }

    private void showDragCursorOnBorders(MouseEvent mouseEvent) {
        if (primaryStage.isMaximized() || primaryStage.isFullScreen() || maximized) {
            this.setCursor(Cursor.DEFAULT);
            return; // maximized mode does not support resize
        }
        if (!primaryStage.isResizable()) {
            return;
        }
        double x = mouseEvent.getX();
        double y = mouseEvent.getY();
        if (contentPlaceHolder.getBorder() != null && contentPlaceHolder.getBorder()
                .getStrokes()
                .size() > 0) {
            double borderWidth = contentPlaceHolder.snappedLeftInset();
            if (isRightEdge(x)) {
                if (y < borderWidth) {
                    this.setCursor(Cursor.NE_RESIZE);
                } else if (y > this.getHeight() - borderWidth) {
                    this.setCursor(Cursor.SE_RESIZE);
                } else {
                    this.setCursor(Cursor.E_RESIZE);
                }
            } else if (isLeftEdge(x)) {
                if (y < borderWidth) {
                    this.setCursor(Cursor.NW_RESIZE);
                } else if (y > this.getHeight() - borderWidth) {
                    this.setCursor(Cursor.SW_RESIZE);
                } else {
                    this.setCursor(Cursor.W_RESIZE);
                }
            } else if (isTopEdge(y)) {
                this.setCursor(Cursor.N_RESIZE);
            } else if (isBottomEdge(y)) {
                this.setCursor(Cursor.S_RESIZE);
            } else {
                this.setCursor(Cursor.DEFAULT);
            }
        }
    }

    private void handleDragEventOnDecoratorPane(MouseEvent mouseEvent) {
        isDragging = true;
        if (!mouseEvent.isPrimaryButtonDown() || (xOffset == -1 && yOffset == -1)) {
            return;
        }
        /*
         * Long press generates drag event!
         */
        if (primaryStage.isFullScreen() || mouseEvent.isStillSincePress() || primaryStage.isMaximized() || maximized) {
            return;
        }

        newX = mouseEvent.getScreenX();
        newY = mouseEvent.getScreenY();

        double deltax = newX - initX;
        double deltay = newY - initY;
        Cursor cursor = this.getCursor();

        if (Cursor.E_RESIZE.equals(cursor)) {
            setStageWidth(initWidth + deltax);
            mouseEvent.consume();
        } else if (Cursor.NE_RESIZE.equals(cursor)) {
            if (setStageHeight(initHeight - deltay)) {
                primaryStage.setY(initStageY + deltay);
            }
            setStageWidth(initWidth + deltax);
            mouseEvent.consume();
        } else if (Cursor.SE_RESIZE.equals(cursor)) {
            setStageWidth(initWidth + deltax);
            setStageHeight(initHeight + deltay);
            mouseEvent.consume();
        } else if (Cursor.S_RESIZE.equals(cursor)) {
            setStageHeight(initHeight + deltay);
            mouseEvent.consume();
        } else if (Cursor.W_RESIZE.equals(cursor)) {
            if (setStageWidth(initWidth - deltax)) {
                primaryStage.setX(initStageX + deltax);
            }
            mouseEvent.consume();
        } else if (Cursor.SW_RESIZE.equals(cursor)) {
            if (setStageWidth(initWidth - deltax)) {
                primaryStage.setX(initStageX + deltax);
            }
            setStageHeight(initHeight + deltay);
            mouseEvent.consume();
        } else if (Cursor.NW_RESIZE.equals(cursor)) {
            if (setStageWidth(initWidth - deltax)) {
                primaryStage.setX(initStageX + deltax);
            }
            if (setStageHeight(initHeight - deltay)) {
                primaryStage.setY(initStageY + deltay);
            }
            mouseEvent.consume();
        } else if (Cursor.N_RESIZE.equals(cursor)) {
            if (setStageHeight(initHeight - deltay)) {
                primaryStage.setY(initStageY + deltay);
            }
            mouseEvent.consume();
        } else if (allowMove) {
            primaryStage.setX(mouseEvent.getScreenX() - xOffset);
            primaryStage.setY(mouseEvent.getScreenY() - yOffset);
            mouseEvent.consume();
        }
    }

    private void updateInitMouseValues(MouseEvent mouseEvent) {
        initStageX = primaryStage.getX();
        initStageY = primaryStage.getY();
        initWidth = primaryStage.getWidth();
        initHeight = primaryStage.getHeight();
        initX = mouseEvent.getScreenX();
        initY = mouseEvent.getScreenY();
        xOffset = mouseEvent.getSceneX();
        yOffset = mouseEvent.getSceneY();
    }

    private boolean isRightEdge(double x) {
        final double width = this.getWidth();
        return x < width && x > width - contentPlaceHolder.snappedLeftInset();
    }

    private boolean isTopEdge(double y) {
        return y >= 0 && y < contentPlaceHolder.snappedLeftInset();
    }

    private boolean isBottomEdge(double y) {
        final double height = this.getHeight();
        return y < height && y > height - contentPlaceHolder.snappedLeftInset();
    }

    private boolean isLeftEdge(double x) {
        return x >= 0 && x < contentPlaceHolder.snappedLeftInset();
    }

    boolean setStageWidth(double width) {
        if (width >= primaryStage.getMinWidth() && width >= buttonsContainer.getMinWidth()) {
            primaryStage.setWidth(width);
            // initX = newX;
            return true;
        } else if (width >= primaryStage.getMinWidth() && width <= buttonsContainer.getMinWidth()) {
            width = buttonsContainer.getMinWidth();
            primaryStage.setWidth(width);
        }
        return false;
    }

    boolean setStageHeight(double height) {
        if (height >= primaryStage.getMinHeight() && height >= buttonsContainer.getHeight()) {
            primaryStage.setHeight(height);
            // initY = newY;
            return true;
        } else if (height >= primaryStage.getMinHeight() && height <= buttonsContainer.getHeight()) {
            height = buttonsContainer.getHeight();
            primaryStage.setHeight(height);
        }
        return false;
    }

    /**
     * set a speficed runnable when clicking on the close button
     *
     * @param onCloseButtonAction runnable to be executed
     */
    public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
        this.onCloseButtonAction.set(onCloseButtonAction);
    }

    /**
     * this property is used to replace JavaFX maximization with a custom one that prevents hiding windows taskbar when
     * the JFXDecorator is maximized.
     *
     * @return customMaximizeProperty whether to use custom maximization or not.
     */
    public final BooleanProperty customMaximizeProperty() {
        return this.customMaximize;
    }

    /**
     * @return whether customMaximizeProperty is active or not
     */
    public final boolean isCustomMaximize() {
        return this.customMaximizeProperty()
                .get();
    }

    /**
     * set customMaximize property
     *
     * @param customMaximize
     */
    public final void setCustomMaximize(final boolean customMaximize) {
        this.customMaximizeProperty()
                .set(customMaximize);
    }

    /**
     * @param maximized
     */
    public void setMaximized(boolean maximized) {
        if (this.maximized != maximized) {
            Platform.runLater(() -> {
                btnMax.fire();
            });
        }
    }

    /**
     * will change the decorator content
     *
     * @param content
     */
    public void setContent(Node content) {
        this.contentPlaceHolder.getChildren()
                .setAll(content);
    }

    public String getTitle() {
        return title.get();
    }

    /**
     * By default this title property is bound to the primaryStage's title property.
     * <p>
     * To change it to something else, use
     * 
     * <pre>
     *     {@code jfxDecorator.titleProperty().unbind();}
     * </pre>
     * 
     * first.
     */
    public StringProperty titleProperty() {
        return title;
    }

    /**
     * If you want the {@code primaryStage}'s title and the {@code JFXDecorator}'s title to be different, then go ahead
     * and use this method.
     * <p>
     * By default, this title property is bound to the {@code primaryStage}'s title property-so merely setting the
     * {@code primaryStage}'s title, will set the {@code JFXDecorator}'s title.
     */
    public void setTitle(String title) {
        this.title.unbind();
        this.title.set(title);
    }

    public void setGraphic(Node node) {
        if (graphic != null) {
            graphicContainer.getChildren()
                    .remove(graphic);
        }
        if (node != null) {
            graphicContainer.getChildren()
                    .add(0, node);
        }
        graphic = node;
    }

    public Node getGraphic() {
        return graphic;
    }

}
